package com.unimod.fsml.model.lexer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.evelopers.unimod.contract.CoreContract;
import com.evelopers.unimod.core.stateworks.Event;
import com.evelopers.unimod.core.stateworks.Guard;
import com.evelopers.unimod.core.stateworks.State;
import com.evelopers.unimod.core.stateworks.StateMachine;
import com.evelopers.unimod.core.stateworks.Transition;
import com.evelopers.unimod.parser.InterpreterException;
import com.evelopers.unimod.runtime.ControlledObject;
import com.evelopers.unimod.runtime.EventProcessorException;
import com.evelopers.unimod.runtime.EventProvider;
import com.evelopers.unimod.runtime.ModelEngine;
import com.evelopers.unimod.runtime.StateMachineConfig;
import com.evelopers.unimod.runtime.StateMachineConfigManager;
import com.evelopers.unimod.runtime.StateMachinePath;
import com.evelopers.unimod.runtime.context.Parameter;
import com.evelopers.unimod.runtime.context.StateMachineContext;
import com.evelopers.unimod.runtime.interpretation.ActionExecutor;
import com.evelopers.unimod.runtime.interpretation.InterpretationEventProcessor;
import com.evelopers.unimod.runtime.interpretation.InterpretationEventProcessor.InterpretationInputActions;
import com.unimod.fsml.FSMLTokens;
import com.unimod.fsml.model.FSMLModelHelper;
import com.unimod.fsml.model.UnhandledTokensResolver;
import com.unimod.fsml.validation.ProblemToken;

/**
 * This class represents lexer for FSML.
 * 
 * @author Ivan Lagunov
 */
public class FSMLLexer implements FSMLTokens, EventProvider, ControlledObject {
    private static final Log log = LogFactory.getLog(FSMLLexer.class);
    
    private StateMachineContext context;
    
	private ModelEngine engine;
	private StateMachine machine;
	private List<Token> tokens;
	private Iterator<Token> tokenIterator;
	private Token prevToken;
	private Token nextToken;
	private State currentState;
	
	/* Data for autocompletion */
	private int offset;
		
	/* Data for validation */
	private List<ProblemToken> problemTokens;
	
	/**
	 * This event provider is designed to be used with build in approach
	 * so it has no default constructor. 
	 */
	public FSMLLexer(StateMachine machine, StateMachineContext context) {
		this.machine = machine;
		this.context = context;
	}

	/* (non-Javadoc)
	 * @see com.evelopers.unimod.runtime.EventProvider#init(com.evelopers.unimod.runtime.ModelEngine)
	 */
	public void init(ModelEngine engine) {
		this.engine = engine;
	}

	/* (non-Javadoc)
	 * @see com.evelopers.unimod.runtime.EventProvider#dispose()
	 */
	public void dispose() {
	}
	
	public void initData(String expr, int offset) {
		tokens = new ArrayList<Token>();
		problemTokens = new ArrayList<ProblemToken>();
		parse(expr);
		tokenIterator = tokens.iterator();
		this.offset = offset;
	}

	/**
	 * Returns the previous token before <code>nextToken</code>. 
	 * 
	 * @return the previous token.
	 */
	public Token getPrevToken() {
		return prevToken;
	}

	/**
	 * Returns the next token after those that were processed before offset. 
	 * 
	 * @return the next token.
	 */
	public Token getNextToken() {
		return nextToken;
	}
	
	/**
	 * Returns an iterator over the collection of found problem tokens.
	 * 
	 * @return an iterator over the collection of found problem tokens.
	 */
	public Iterator<ProblemToken> getProblemTokenIterator() {
		return problemTokens.iterator();
	}
	
	/**
	 * @unimod.action.descr Number of unconsumable tokens
	 */
	public int x1(StateMachineContext context) throws EventProcessorException, InterpreterException {
		Set<Integer> consumableTokens = getConsumableTokens();
	    int unconsumableTokensNumber = 1;
	    // TODO: check if tokenIterator is correct.
	    for (Iterator<Token> i = tokenIterator; i.hasNext();) {
	    	Token token = i.next();
	    	if (!consumableTokens.contains(token.getTypeId())) {
	            unconsumableTokensNumber++;
	    	} else {
	    		break;
	    	}
	    }
		return unconsumableTokensNumber;
	}
	
	/**
	 * Returns set of consumable tokens in current state.
	 * 
	 * @return the set of consumable tokens.
	 * @throws EventProcessorException 
	 * @throws InterpreterException 
	 */
    public Set<Integer> getConsumableTokens() throws EventProcessorException, InterpreterException {
        Set<Integer> consumableTokens = new HashSet<Integer>();
		for (State cur = getActiveState(context); cur != null; cur = cur.getSuperstate()) {
			for (Object o : cur.getOutgoingTransitions()) {
				Transition transition = (Transition) o;
				if (!isErroneousTransition(transition) && guardConditionIsMet(transition)) {
					consumableTokens.add(Events.getIdByName(transition.getEvent().getName()));
				}
			}
		}
		return consumableTokens;
	}

    /**
     * Checks if last token was valid according to state machine or not.
     * If not valid, it should be changed.
     * 
     * @param lastTokenType the type of last token.
     * @return true if last token is valid.
     */
    public boolean isValidLastToken(String lastTokenType) {
		Transition elseTransition = null;
    	try {
    		State prevState = currentState;
			while ((prevState != null) && (null == elseTransition)) {
				for (Object o : prevState.getOutgoingTransitions()) {
				    Transition transition = (Transition) o;
				    if (lastTokenType.equals(transition.getEvent().getName())) {
				    	if (transition.getGuard().equals(Guard.ELSE)) {
				    		elseTransition = transition; 
				    	} else if (guardConditionIsMet(transition)) {
					    	return !isErroneousTransition(transition);
				    	}
				    }
				}
				
				prevState = prevState.getSuperstate();
			}
		} catch (InterpreterException e) {
			log.error("Cannot check guard condition", e);
		}
		return (elseTransition != null) ? !isErroneousTransition(elseTransition) : false;
    }

    private boolean isErroneousTransition(Transition transition) {
    	return UnhandledTokensResolver.ERROR_TRANSITION_NAME.equals(transition.getName());
    }
    
    private boolean guardConditionIsMet(Transition transition) throws InterpreterException {
    	FSMLModelHelper modelHelper = FSMLModelHelper.getInstance();
    	InterpretationEventProcessor interpretationEventProcessor = new InterpretationEventProcessor(modelHelper.getProgramModel());
        // Prepare input actions pool
    	ActionExecutor actionExecutor = new ActionExecutor(modelHelper.getCOMap());
		StateMachinePath path = getStateMachinePath();
		InterpretationInputActions inputActions = interpretationEventProcessor.new InterpretationInputActions(actionExecutor, path);
    	
    	return interpretationEventProcessor.guardConditionIsMet(transition, context, inputActions);
    }

    /**
	 * Returns token from the stream.
	 * If index equals to 0 then last token will be returned, 
	 * if index equals to 1 then last but one token will be returned,
	 * etc.
	 *   
	 * @param index index from the end of stream.
	 * @return peeked token.
	 */
	public Token peekToken(int index) {
		return 0 <= index && index < tokens.size() ? (Token) tokens.get(tokens.size() - (index + 1)) : null;
	}

	/**
	 * Processes tokens before the offset specified during <code>initData</code> call.
	 * 
	 * @return whether there is any token left to process.
	 */
	public boolean processTokensBeforeOffset() {
		try {
			currentState = getActiveState(context);
		} catch (EventProcessorException e) {
			log.error("Cannot load active state", e);
		}

		while (hasNextTokenBeforeOffset()) {
            tick();
        }
		return (null == nextToken);
	}	

	/**
	 * Processes the rest of tokens. 
	 */
	public void processTokensAfterOffset() {
		if (nextToken != null) {
			do {
				tick();
			} while (hasNextTokenAfterOffset());
		}
	}
		
    /**
     * Checks if there is a token strictly before given offset and saves it.
     * 
     * @return true - if next token exists, false - otherwise.
     */
	private boolean hasNextTokenBeforeOffset() {
		prevToken = nextToken;
		if (tokenIterator.hasNext()) {
			nextToken = tokenIterator.next();
			
			return (nextToken.getEnd() < offset) || 
				((nextToken.getEnd() == offset) && (!nextToken.isSpaceFollowed()));
		} else {
			nextToken = null;
			return false;
		}
	}
	
    /**
     * Checks if there is a token left and saves it.
     * 
     * @return true - if next token exists, false - otherwise.
     */
	private boolean hasNextTokenAfterOffset() {
		prevToken = nextToken;
		if (tokenIterator.hasNext()) {
			nextToken = tokenIterator.next();
			return true;
		} else {
			nextToken = null;
			return false;
		}
	}

	private void tick() {
        Parameter ptoken = new Parameter(LexerParameters.TOKEN, nextToken);
        Parameter expr = new Parameter(LexerParameters.EXPRESSION, nextToken.getExpr());
        Parameter parsedExpr = new Parameter(LexerParameters.PARSED_EXPRESSION, nextToken.getParsedExpr());
        Parameter tokenType = new Parameter(LexerParameters.TOKEN_TYPE, nextToken.getType());
        Parameter tokenValue = new Parameter(LexerParameters.TOKEN_VALUE, nextToken.getValue());
        
        Event token = new Event(nextToken.getType(), new Parameter[] {
                ptoken, expr, parsedExpr, tokenType, tokenValue});

        checkForProblem(nextToken);

        engine.getEventManager().handle(token, context);

        try {
			currentState = getActiveState(context);
		} catch (EventProcessorException e) {
			log.error("Cannot load active state", e);
		}
	}
	
    private void checkForProblem(Token lastToken) {
    	if (!isValidLastToken(lastToken.getType())) {
    		problemTokens.add(new ProblemToken(lastToken));
    	}
    }
    
	/**
	 * Returns active state by given {@link StateMachineContext context}.
	 * 
	 * @param context state machine context.
	 * @return the active state.
	 * @throws EventProcessorException 
	 */
	private State getActiveState(StateMachineContext context) throws EventProcessorException {
		StateMachinePath path = getStateMachinePath();
        StateMachineConfigManager configManager = engine.getEventProcessor().getModelStructure().getConfigManager(machine.getName());
        StateMachineConfig config = configManager.load(path, context);
		
        return CoreContract.decodeState(machine, config.getActiveState());
	}
	
	private StateMachinePath getStateMachinePath() {
		return new StateMachinePath(machine.getName());
	}
	
	private static Pattern TOKEN_PATTERN = Pattern.compile("(-?\\d+)|([_A-Za-z]\\w*)|(&&)|(\\|\\|)|(([><]=?)|([!=]=))|(\\.)|(\\!)|(\\,)|(\\;)|(\\{)|(\\})|(\\()|(\\))|(\\s+)|(//.*?$|/\\*.*?\\*/)", Pattern.DOTALL | Pattern.MULTILINE);

	private void parse(String expr) {
		Matcher matcher = TOKEN_PATTERN.matcher(expr);
		int pos = 0;
		int lineNumber = 0;
		int nextNewLineCharPos = -1;
		String type = null;
		
		while (matcher.find()) {
		    type = null;

			if (matcher.group(1) != null) {
				type = E4_NAME;
			} else if (matcher.group(2) != null) {
				String value = matcher.group();
				if (Arrays.asList(BOOL_CONSTS).contains(value)) {
					type = E3_NAME;
				} else if (EVENT_TYPE.ENTER.equals(value)) {
					type = E26_NAME;
				} else if (EVENT_TYPE.ANY.equals(value)) {
					type = E27_NAME;
				} else if (EVENT_TYPE.EXIT.equals(value)) {
					type = E28_NAME;
				} else if (STATE_TYPE.INITIAL.equals(value)) {
					type = E18_NAME;
				} else if (STATE_TYPE.FINAL.equals(value)) {
					type = E19_NAME;
				} else if (EXECUTE.equals(value)) {
					type = E20_NAME;
				} else if (TRANSITTO.equals(value)) {
					type = E21_NAME;
				} else if (ON.equals(value)) {
					type = E22_NAME;
				} else if (STATEMACHINE.equals(value)) {
					type = E23_NAME;
				} else if (IF.equals(value)) {
					type = E24_NAME;
				} else if (ELSE.equals(value)) {
					type = E25_NAME;
				} else if (USES.equals(value)) {
					type = E29_NAME;
				} else if (INCLUDE.equals(value)) {
					type = E30_NAME;
				} else {
					type = E5_NAME;
				}			
			} else if (matcher.group(3) != null) {
				type = E6_NAME;
			} else if (matcher.group(4) != null) {
				type = E7_NAME;
			} else if (matcher.group(5) != null) {
				type = E8_NAME;
			} else if (matcher.group(8) != null) {
				type = E10_NAME;
			} else if (matcher.group(9) != null) {
				type = E11_NAME;
			} else if (matcher.group(10) != null) {
				type = E12_NAME;
			} else if (matcher.group(11) != null) {
				type = E13_NAME;
			} else if (matcher.group(12) != null) {
				type = E14_NAME;
			} else if (matcher.group(13) != null) {
				type = E15_NAME;
			} else if (matcher.group(14) != null) {
				type = E16_NAME;
			} else if (matcher.group(15) != null) {
				type = E17_NAME;
			} else if (matcher.group(16) != null) {
				// Blank token.
//				type = E31_NAME;
			} else if (matcher.group(17) != null) {
				// Comment token.
//				type = E32_NAME;
			}
				
			int tokenStart = matcher.start();
			int tokenEnd = matcher.end();
			while (tokenStart > nextNewLineCharPos) {
				lineNumber++;
				nextNewLineCharPos = StringUtils.indexOf(expr, '\n', nextNewLineCharPos + 1);
				if (-1 == nextNewLineCharPos) {
					nextNewLineCharPos = expr.length() + 1;
				}
			}
			
			// Some part of text wasn't matched by pattern.
			if (pos < tokenStart) {
				tokens.add(createUnresolvedToken(expr, pos, tokenStart, lineNumber));
			}
			
			log.debug(String.format("Found token: [%s]", matcher.group()));

			if (type != null) {
				Token token = new Token(type, matcher.group(), tokenStart, tokenEnd, lineNumber, expr);
				tokens.add(token);
			}
            pos = tokenEnd;
		}

		if (pos < expr.length()) {
			tokens.add(createUnresolvedToken(expr, pos, expr.length(), lineNumber));
		}
	}
	
	private Token createUnresolvedToken(String expr, int start, int end, int lineNumber) {
		log.debug(String.format("Unresolved char sequence [%d,%d] = %s", start, end - 1, expr.substring(start, end)));				

		return new Token(E1_NAME, expr.substring(start, end), start, end, lineNumber, expr);
	}
}
